# -*- coding: utf-8 -*-
# @name             : 用例同步
# @author           : OneYoung
# @create           : 2024/07/18
# @version          : v1.0
# @desc             : 从swagger接口文档同步接口生成用例文件

import os.path
import requests
from utils.log_record import *
from utils.file_reader import *
from script.template.case import *
from application.settings import SWAGGER_URL, BASE_DIR, CASE_DIR


# 同步基础信息
class SynchronizationInfo:
    # 获取上一次更新版本信息
    @staticmethod
    def get_old_version():
        versionPath = os.path.join(BASE_DIR, 'static/version')  # 版本文件路径
        # 不存在时创建
        if not os.path.exists(versionPath):
            os.makedirs(versionPath)
        # 获取路径
        try:
            version_dir = os.listdir(versionPath)  # 版本文件夹列表
            version_dir.sort(key=lambda fn: os.path.getctime(versionPath + '\\' + fn))  # 排序
            dir_path = os.path.join(versionPath, version_dir[-1])  # 获取最新文件夹路径
        except (FileNotFoundError, IndexError):
            dir_path = versionPath
        return dir_path


# 获取API列表
class GetApiList:
    sc = SynchronizationInfo()

    # 从swagger网页中读取接口
    @staticmethod
    def read_swagger_page():
        json_data = {}
        requests.timeout = 10  # 超时时间
        try:
            response = requests.get(SWAGGER_URL)
            if response.status_code == 200:
                try:
                    json_data = response.json()
                except Exception as ec:
                    logger.error(f'获取接口信息失败:{response.url},{ec}')
            else:
                logger.error(f'接口地址不正确:{SWAGGER_URL}')
        except Exception as ec:
            logger.error(f"接口地址访问失败:{SWAGGER_URL}\n-> {ec}")
        return json_data

    # 从json信息中读取接口列表
    @staticmethod
    def get_swagger_api(json_data):
        analysis = AnalysisData()
        api_list = []
        if json_data != {}:
            try:
                paths = json_data["paths"]
                schemas = json_data['components']['schemas']
                for api_url, api_infos in paths.items():
                    # 获取接口地址
                    for method, r_Infos in api_infos.items():
                        tags = r_Infos['tags'][0]
                        api_info = {}
                        try:
                            api_name = r_Infos["summary"].replace('\r', '').replace('\n', '').replace(' ', '')
                        except KeyError:
                            api_name = ""
                        api_info["tags"] = tags  # 获取接口分类
                        api_info["api_name"] = api_name  # 获取接口名称
                        api_info["api_url"] = api_url  # 获取接口地址
                        api_info["method"] = method  # 获取请求方法
                        # 是否弃用
                        try:
                            deprecated = r_Infos['deprecated']
                        except KeyError:
                            deprecated = False
                        if not deprecated:
                            # 获取params数据
                            try:
                                parameters = r_Infos["parameters"]
                                paramInfos = analysis.analysis_params_data(schemas, parameters)
                                if str(paramInfos):
                                    api_info["params"] = str(paramInfos)
                                else:
                                    api_info["params"] = "{}"
                            except KeyError:
                                api_info["params"] = "{}"

                            # 获取data数据
                            try:
                                dataInfos = {}
                                try:
                                    requestBody = r_Infos['requestBody']['content']['multipart/form-data']['schema'][
                                        'properties']
                                    for keys, values in requestBody.items():
                                        try:
                                            des = values['description'].replace('\n', '').replace('\r', '').replace(' ',
                                                                                                                    '')
                                            description = des.replace('（', '(').replace('）', ')')
                                            dataInfos[keys] = description
                                        except KeyError:
                                            data = values['type']
                                            dataInfos[keys] = data
                                except KeyError:
                                    try:
                                        schema = r_Infos['requestBody']['content']['application/json']['schema']
                                    except KeyError:
                                        schema = r_Infos['requestBody']['content']['application/json; version=1.0'][
                                            'schema']
                                    try:
                                        try:
                                            requestBody = schema['$ref']
                                        except KeyError:
                                            requestBody = schema['items']['$ref']
                                        bodyName = requestBody.split('/')[-1]
                                        dataInfos = analysis.analysis_request_data(schemas, bodyName, dataInfos)
                                    except KeyError:
                                        dataInfos = {}
                                    try:
                                        if schema['type'] == 'array':
                                            dataInfos = [dataInfos]
                                    except KeyError:
                                        pass
                                api_info["data"] = str(dataInfos)
                            except KeyError:
                                api_info["data"] = '{}'
                            # 将接口相关数据存放到列表中
                            api_info["remark"] = 'Update'
                            api_list.append(api_info)
            except KeyError:
                logger.error('接口信息为空.')
        return api_list


# 解析数据
class AnalysisData:
    # 获取枚举值数据
    @staticmethod
    def get_enum(request_body, bodyName):
        enum_data = request_body[bodyName]
        try:
            enum = enum_data['enum']
            r_enum = str(enum).replace('\n', '').replace('\r', '').replace(' ', '')
            enumInfo = r_enum.replace('（', '(').replace('）', ')')
            try:
                try:
                    des = enum_data['description']
                except KeyError:
                    des = enum_data['x-enumDescriptions']
                    if des[0] == '':
                        des = enum_data['x-enumNames']
            except KeyError:
                des = ''
            try:
                data_type = enum_data['type']
            except KeyError:
                data_type = 'string'
            r_des = str(des).replace('\n', '').replace('\r', '').replace(' ', '').replace('（', '(').replace('）', ')')
            values = enumInfo + r_des + "<" + data_type + ">"
        except KeyError:
            values = ""
        return values

    # 获取描述信息
    @staticmethod
    def get_description(values):
        try:
            if values['nullable']:
                nullable = ''
            else:
                nullable = '*'
        except KeyError:
            nullable = '*'
        try:
            des = values['description'].replace('\n', '').replace('\r', '').replace(' ', '')
            description = des.replace('（', '(').replace('）', ')')
        except KeyError:
            description = ''
        try:
            value_type = values['type']
        except KeyError:
            value_type = 'string'
        if nullable != '' or description != '':
            data = "  # " + nullable + description + "<" + value_type + ">"
        else:
            data = "  # <" + value_type + "> "
        return data

    # 解析params数据
    def analysis_params_data(self, schemas, parameters):
        paramInfos = {}
        try:
            for parameter in parameters:
                param = parameter["name"]  # 入参名称
                if param != "version":
                    try:
                        if parameter['required']:
                            required = '*'
                        else:
                            required = ''
                    except KeyError:
                        required = ''
                    try:
                        schema_ref = parameter['schema']['$ref']
                        ref_name = schema_ref.split('/')[-1]
                        description = self.get_enum(schemas, ref_name)
                    except KeyError:
                        try:
                            schema_type = parameter['schema']['type']
                        except KeyError:
                            schema_type = ''
                        try:
                            des = parameter["description"].replace('\n', '').replace('\r', '').replace(' ', '')
                            description = des.replace('（', '(').replace('）', ')') + " <" + schema_type + ">"
                        except KeyError:
                            description = '' + "<" + schema_type + ">"
                    if required != '' or description != '':
                        parameter = "  # " + required + description
                    else:
                        parameter = "  # "
                    paramInfos[param] = parameter
        except KeyError:
            logger.error("获取接口parameters入参内容为空.")
        return paramInfos

    """↓↓↓解析DATA数据↓↓↓"""

    # 获取data的数据集
    def get_properties(self, request_body, bodyName):
        p_array = {}
        try:
            p_data = request_body[bodyName]['properties']
            for keys, values in p_data.items():
                p_array[keys] = values
        except KeyError:
            values = self.get_enum(request_body, bodyName)
            p_array['enum'] = values
        return p_array

    # 解析data数据
    def analysis_request_data(self, request_body, bodyName, dataInfos):
        try:
            bodyData = request_body[bodyName]['properties']
            for keys, values in bodyData.items():
                try:
                    try:
                        ref_name = values['$ref'].split('/')[-1]
                    except KeyError:
                        ref_name = values['items']['$ref'].split('/')[-1]
                    ref_array = self.get_properties(request_body, ref_name)
                    new_array = {}
                    for p_keys, p_values in ref_array.items():
                        if p_keys == 'enum':
                            new_array = "  # *" + p_values
                        else:
                            try:
                                ref = (eval(str(p_values)))['$ref'].split('/')[-1]
                                dataArray = {}
                                properties = self.analysis_request_data(request_body, ref, dataArray)
                            except TypeError:
                                properties = p_values
                            except KeyError:
                                properties = self.get_description(p_values)
                            new_array[p_keys] = properties
                            try:
                                if eval(str(p_values))['type'] == 'array':
                                    new_array[p_keys] = [properties]
                                else:
                                    new_array[p_keys] = properties
                            except KeyError:
                                new_array[p_keys] = properties
                    data = new_array
                except KeyError:
                    data = self.get_description(values)
                try:
                    if values['type'] == 'array':
                        dataInfos[keys] = [data]
                    else:
                        dataInfos[keys] = data
                except KeyError:
                    dataInfos[keys] = data
        except KeyError:
            values = self.get_enum(request_body, bodyName)
            dataInfos = "  # *" + values
        return dataInfos


# 版本对比
class VersionContrasted:
    sc = SynchronizationInfo()

    # 读取旧的接口内容
    @staticmethod
    def get_old_values(excel_file):
        values = {}
        if os.path.exists(excel_file):
            try:
                wb = openpyxl.load_workbook(excel_file)
                sheet = wb['Sheet1']
                values = list(sheet.values)
            except Exception as ec:
                logger.error(str(ec))
        return values

    # 获取更新接口
    def get_new_api(self, api_list):
        # 读取旧的接口内容
        dir_path = self.sc.get_old_version()
        old_file = os.path.join(dir_path, 'api_data.xlsx')  # 获取最新文件路径
        old_values = self.get_old_values(old_file)
        # 读取最新接口信息
        new_list = []
        for api_info in api_list:
            n_name = api_info['api_name']
            n_url = api_info['api_url']
            n_method = api_info['method']
            n_params = api_info['params']
            n_data = api_info['data']
            url_list = []
            name_list = []
            for value in old_values:
                # 执行对比
                if n_name == value[1] and n_url == value[3] and n_method == value[4]:
                    if eval(str(n_params)) != eval(value[5]):
                        try:
                            differ = set(eval(str(n_params)).items()) ^ set(eval(value[5]).items())
                            api_info['remark'] = f'入参更新:{differ}'
                        except (TypeError, AttributeError):
                            pass
                        new_list.append(api_info)
                    if eval(str(n_data)) != eval(value[6]):
                        try:
                            differ = set(eval(str(n_data)).items()) ^ set(eval(value[5]).items())
                            api_info['remark'] = f'入参更新:{differ}'
                        except (TypeError, AttributeError):
                            pass
                        new_list.append(api_info)
                url_list.append(value[3])
                name_list.append(value[1])
            if n_url not in url_list:
                api_info['remark'] = 'New Api'
                new_list.append(api_info)
            if n_url in url_list and n_name not in name_list:
                api_info['remark'] = '接口名称更新'
                new_list.append(api_info)
        # print(new_list)
        return new_list

    # 处理已删除接口
    @staticmethod
    def dispose_del_api_file(tags, api_url, method):
        cf = CaseFile()
        # 文件名
        joint_info = cf.joint_name(api_url, method)
        file_name = joint_info["file_name"]
        # 路径
        file_path = os.path.join(CASE_DIR, f'/{tags}')

        # 作废用例文件
        for root, dirs, files in os.walk(file_path):  # 查找路径下删除接口对应的用例文件
            for file in files:
                if file_name == file:
                    date = time.strftime("%y%m%d")
                    new_file = f'del{date}_{file}'  # 文件名前增加删除标识
                    source_file = os.path.join(root, file)
                    delete_file = os.path.join(root, new_file)
                    os.rename(source_file, delete_file)  # 重命名文件，以此方式作废用例文件
                    del_path = root.replace("\\", "/").split("testCase/")[1] + ":" + file
                    msg = f"作废此接口对应用例:{del_path}"
                    logger.info(msg)

    # 获取已删除接口用例
    def get_del_api(self, api_info):
        # 获取最新接口url列表
        new_api_list = []
        for api in api_info:
            new_api_list.append(api['api_url'])
        # 读取旧的接口列表
        dir_path = self.sc.get_old_version()
        api_file = os.path.join(dir_path, 'api_data.xlsx')  # 获取最新文件路径
        old_api = self.get_old_values(api_file)
        if new_api_list:
            # 读取删除接口信息
            for value in old_api:
                if value[0] != 'case_no':  # 跳过非接口信息行
                    # 执行对比
                    if value[3] not in new_api_list:
                        msg = f"接口已删除:{value[3]}({value[1]})"
                        logger.info(msg)
                        # 处理接口用例文件
                        self.dispose_del_api_file(value[2], value[3], value[4])
        logger.info("接口用例同步完成.\n")


# 保存版本文件
class SaveVersionFiles:
    sc = SynchronizationInfo()
    vc = VersionContrasted()

    # 初始化文件保存路径
    def __init__(self):
        date = time.strftime('%Y-%m-%d', time.localtime())
        self.version_folder = os.path.join(BASE_DIR, f'static/version/{date}')
        if not os.path.exists(self.version_folder):
            os.makedirs(self.version_folder)

    # 保存为json文件
    def save_to_swagger_json(self):
        try:
            jsonInfo = GetApiList().read_swagger_page()
            jsonData = json.dumps(jsonInfo, indent=4, ensure_ascii=False)
            # 保存swagger.json文件
            file_name = "swagger.json"
            filePath = os.path.join(self.version_folder, file_name)
            with open(filePath, 'w', encoding='utf-8') as f:
                f.write(jsonData)
            logger.info(f'{file_name}保存成功.')
        except Exception as ec:
            logger.error(f'保存文件失败:{ec}')

    # 将数据保存到Excel中
    def write_to_excel(self, api_list, file_name):
        files = openpyxl.Workbook()
        sheet = files.active
        sheet.title = 'Sheet1'
        # 添加标题
        title = ['case_no', 'case_name', 'tags', 'api_url',
                 'method', 'params', 'data', 'file', 'assert_info']
        sheet.append(title)

        # 循环读取接口数据保存表格中
        for n in range(len(api_list)):
            num = str(n + 1)
            if len(num) < 4:
                num = "0" * (3 - len(num)) + num
            case_no = "case" + num
            case_name = api_list[n]['api_name']
            tags = api_list[n]['tags']
            api_url = api_list[n]['api_url']
            method = api_list[n]['method']
            params = api_list[n]['params']
            data = api_list[n]['data']
            api_info = [case_no, case_name, tags, api_url, method, str(params), str(data), '{}', '{}']
            sheet.append(api_info)
        # 保存路径
        file_path = os.path.join(self.version_folder, file_name)
        if os.path.exists(file_path):
            os.remove(file_path)
        files.save(file_path)
        logger.info(f"{file_name}保存成功.")

    # 保存接口数据到Excel中
    def save_version_to_excel(self):
        # 获取api列表
        g = GetApiList()
        json_data = g.read_swagger_page()
        api_list = g.get_swagger_api(json_data)
        if api_list:
            # 按业务分别保存对应的API接口
            self.write_to_excel(api_list, 'api_data.xlsx')


# 用例文件管理
class CaseFile:
    # 创建文件，写入数据
    @staticmethod
    def create_files(case_file, case_info):
        # 已存在时重命名
        if os.path.exists(case_file):
            path = case_file.split('test_')[0]
            date = time.strftime('%y%m%d', time.localtime())
            rename = case_file.split('.py')[0] + str(date) + ".py"
            case_file = os.path.join(path, rename)
        with open(case_file, 'w', encoding='utf-8') as f:
            for line in case_info:
                f.writelines(line)

    # 根据请求地址和请求方式生成相应名称
    @staticmethod
    def joint_name(api_url, method):
        joint_body = {}
        # 拆分url
        elements = api_url.split('/')

        # 对接口地址中的字母大小写做处理，生成的类名称美观些
        element_list = []
        for element in elements[1:]:
            letter = element[0]
            el = element.replace('_', '')  # 处理部分接口有下横杠的
            if letter.istitle():
                element_list.append(el)
            else:
                element_list.append(el.title())

        # 组装地址
        class_name = "".join(element_list)  # 用例集类名
        def_name = 'test01' + '_'.join(elements)  # 用例函数名称
        file_name = 'test' + '_'.join(elements) + '_' + method + '.py'  # 用例文件名
        joint_body["api_url"] = api_url  # API地址
        joint_body["catalog"] = elements[1]  # 目录, 用例细分用例
        joint_body["class_name"] = class_name  # 类名
        joint_body["def_name"] = def_name  # 用例名称
        joint_body["file_name"] = file_name  # 用例文件名称
        return joint_body

    # 生成用例文件
    def generated_case_file(self, new_api, save_path, is_all, auto):
        vc = VersionContrasted()
        # 获取接口列表信息
        api_list = GetApiList().get_swagger_api(new_api)
        if api_list:
            if is_all not in ["Y", "y"]:
                new_list = vc.get_new_api(api_list)
            else:
                new_list = api_list

            # 获取接口列表信息
            if new_list:
                # 组建用例内容
                logger.info(f"开始同步接口用例...")
                for api_data in new_list:
                    tags = api_data['tags']  # 接口分类
                    api_name = api_data['api_name']  # 接口名称
                    api_url = api_data['api_url']  # 接口地址
                    method = api_data['method']  # 请求方法
                    params = api_data['params']  # param参数
                    data = api_data['data']  # data参数
                    remark = api_data['remark']  # 更新说明

                    # 获取拼接信息
                    joint_infos = self.joint_name(api_url, method)

                    # 用例文件保存地址，不存在时创建
                    save_catalog = os.path.join(save_path, f'{tags}')  # 可细分用例，增加catalog，tags/catalog
                    if not os.path.exists(save_catalog):
                        os.makedirs(save_catalog)
                        initFile = os.path.join(save_catalog, "__init__.py")
                        self.create_files(initFile, "")
                    file_name = joint_infos['file_name']
                    file_path = os.path.join(save_catalog, file_name)

                    # 获取接口入参信息
                    if params != '{}' and data == '{}':
                        params_data = eval(params)  # 格式化
                        body_data = {}
                        p_format = 'params'
                    elif params == '{}' and data != '{}':
                        params_data = {}
                        body_data = eval(data)  # 格式化
                        p_format = 'body'
                    elif params != '{}' and data != '{}':
                        params_data = eval(params)  # 格式化
                        body_data = eval(data)
                        p_format = 'params_body'
                    else:
                        params_data = {}
                        body_data = {}
                        p_format = ''

                    # 生成用例信息
                    class_name = joint_infos['class_name']
                    def_name = joint_infos['def_name']
                    case_info = creat_case_model(
                        api_name,
                        class_name,
                        joint_infos['api_url'],
                        def_name,
                        params_data,
                        body_data,
                        method,
                        p_format
                    )
                    # 生成用例文件
                    msg = f"接口用例同步成功:{file_name};[{remark}]"
                    if auto in ["Y", "y"]:
                        self.create_files(file_path, case_info)
                        logger.info(msg)
                    else:
                        logger.info(f"正在更新接口:{file_name}")
                        affirm = input("是否确认更新(Y/N):")
                        if affirm in ["Y", "y"]:
                            self.create_files(file_path, case_info)
                            logger.info(msg)
                        else:
                            logger.info(f"已取消更新:{file_name}")
                # 获取并处理已删除接口
                vc.get_del_api(api_list)
            else:
                logger.info("无更新接口.")


# 同步测试用例
class SynchronizationCase:
    sc = SynchronizationInfo()
    ga = GetApiList()

    # 从Swagger网页同步测试用例
    def run_write_case_from_web(self, is_all, auto):
        """
        is_all="Y"时全部更新，否则只更新有变动的Api
        auto="Y"时，默认强制更新，否则需要手动确认是否进行更新
        """
        new_api = self.ga.read_swagger_page()
        # 路径不存在时创建
        if not os.path.exists(CASE_DIR):
            os.makedirs(CASE_DIR)
            init_file = os.path.join(CASE_DIR, "__init__.py")
            CaseFile().create_files(init_file, "")
        CaseFile().generated_case_file(new_api, CASE_DIR, is_all, auto)


if __name__ == '__main__':
    # 同步接口测试用例
    s_case = SynchronizationCase()
    # 从网页同步接口
    s_case.run_write_case_from_web(is_all='N', auto='Y')

    # # 保存版本文件
    # sv = SaveVersionFiles()
    # # 保存json文件
    # sv.save_to_swagger_json()
    # # 保存为Excel版本文件
    # sv.save_version_to_excel()
